﻿using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Collections.Generic;
using System.Windows.Controls.Primitives;
using System.Linq;
using System.Diagnostics;
using System.Collections.Specialized;

namespace SStuff.FlowControls
{
    /// <summary>
    /// Base class for FlowPanel2D / 3D (handles common logic, such as Virtualization)
    /// </summary>
    public abstract class FlowPanelBase : VirtualizingPanel
    {
        private double _layoutOffset;
        private int _itemCount;
        private int _firstVisibleIndex, _lastVisibleIndex;

        /// <summary>
        /// Get or set the Layout offset
        /// </summary>
        public double LayoutOffset
        {
            get { return _layoutOffset; }
            set
            {
                _layoutOffset = value;
                InvalidateMeasure();
            }
        }

        private bool _cyclicMode;

        /// <summary>
        /// Get or set the Cyclic mode
        /// </summary>
        public bool CyclicMode
        {
            get { return _cyclicMode; }
            set
            {
                _cyclicMode = value;
                InvalidateMeasure();
            }
        }


        /// <summary>
        /// Get or set the total item count
        /// </summary>
        public int ItemCount
        {
            get { return _itemCount; }
            set
            {
                _itemCount = value;
                InvalidateMeasure();
            }
        }

        private double _consideredItems;

        /// <summary>
        /// Get or set the number of items to consider in a layout pass
        /// </summary>
        public double ConsideredItems
        {
            get { return _consideredItems; }
            set { _consideredItems = value; }
        }

        /// <summary>
        /// Send the size of the viewport to the underlaying layout algorithm
        /// </summary>
        /// <param name="availableSize"></param>
        protected abstract void SendAvailableSizeToAlgorithm(Size availableSize);


        /// <summary>
        /// Provides the behavior for the Measure pass of Silverlight layout. Classes can override this method to define their own Measure pass behavior.
        /// </summary>
        /// <returns>
        /// The size that this object determines it needs during layout, based on its calculations of child object allotted sizes, or possibly on other considerations such as fixed container size.
        /// </returns>
        /// <param name="availableSize">The available size that this object can give to child objects. Infinity (<see cref="F:System.Double.PositiveInfinity"/>) can be specified as a value to indicate that the object will size to whatever content is available.</param>
        protected override Size MeasureOverride(Size availableSize)
        {
            // layout only work with a constrained size
            if (double.IsPositiveInfinity(availableSize.Width) || double.IsPositiveInfinity(availableSize.Height) || ItemCount == 0)
            {
                return new Size(0, 0);
            }

            SendAvailableSizeToAlgorithm(availableSize);

            //// handle the virtualization
            CalculateIndices();

            UpdateVisibleRanges();


            if (ItemsControl.GetItemsOwner(this).Items.Count == 0)
                return availableSize;
            //// get the visible ranges and apply layout on them
            for (int i = 0; i < _usedRanges; i++)
            {
                EnsureChildrenCreated(_visibleRanges[i * 2], _visibleRanges[i * 2 + 1], availableSize);
            }


            for(int i=Children.Count-1;i>=0;i--)
            {                
                GeneratorPosition childGeneratorPosition = new GeneratorPosition(i, 0);
                int itemIndex = ItemContainerGenerator.IndexFromGeneratorPosition(childGeneratorPosition);
                if(!IsInVisibleRange(itemIndex))
                {
                    ItemContainerGenerator.Remove(childGeneratorPosition, 1);
                    RemoveInternalChildRange(i, 1);
                }
            }

            

            ApplyZOrder(Children.OfType<UIElement>());

            return availableSize;
        }

        protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
        {
            switch (args.Action)
            {
                case NotifyCollectionChangedAction.Remove:
                case NotifyCollectionChangedAction.Replace:
                    RemoveInternalChildRange(args.Position.Index, args.ItemUICount);
                    break;
            }
        }

        private void EnsureChildrenCreated(int firstVisibleIndex, int lastVisibleIndex, Size availableSize)
        {
            var children = Children;
            IItemContainerGenerator generator = ItemContainerGenerator;
            GeneratorPosition start = generator.GeneratorPositionFromIndex(firstVisibleIndex);
            int childIndex = (start.Offset == 0) ? start.Index : start.Index + 1;
            using (generator.StartAt(start, GeneratorDirection.Forward, true))
            {
                for (int i = firstVisibleIndex; i <= lastVisibleIndex; ++i, ++childIndex)
                {
                    bool isNewlyRealized;

                    UIElement child = generator.GenerateNext(out isNewlyRealized) as UIElement;
                    if (isNewlyRealized)
                    {
                        if (childIndex >= children.Count)
                        {
                            base.AddInternalChild(child);
                        }
                        else
                        {
                            base.InsertInternalChild(childIndex, child);
                        }
                        generator.PrepareItemContainer(child);
                    }
                    else
                    {
                        Debug.Assert(child == children[childIndex]);
                    }

                    child.Measure(availableSize);
                    double relativeItemIndex = CalculateRelativeIndex(ItemsControl.GetItemsOwner(this).ItemContainerGenerator.IndexFromContainer(child));


                    ApplyLayout(relativeItemIndex, child, (int)_consideredItems);
                }
            }
        }

        /// <summary>
        /// Apply the Z order to the flow items
        /// </summary>
        /// <param name="children"></param>
        protected abstract void ApplyZOrder(IEnumerable<UIElement> children);

        /// <summary>
        /// Unreference the containers that need to be recycled and signal them as recycled
        /// </summary>
       


        /// <summary>
        /// Provides the behavior for the Arrange pass of Silverlight layout. Classes can override this method to define their own Arrange pass behavior.
        /// </summary>
        /// <returns>
        /// The actual size used once the element is arranged in layout.
        /// </returns>
        /// <param name="finalSize">The final area within the parent that this object should use to arrange itself and its children.</param>
        protected override Size ArrangeOverride(Size finalSize)
        {
            foreach (UIElement child in Children)
            {

                ArrangeChild(finalSize, child);
            }
            return finalSize;
        }

       
        /// <summary>
        /// Arrange a child item
        /// </summary>
        /// <param name="finalSize">size of the viewport</param>
        /// <param name="child">item to draw</param>
        protected virtual void ArrangeChild(Size finalSize, UIElement child)
        {
            if (child.DesiredSize.Height == 0.0 || child.DesiredSize.Width == 0.0)
                return;
            child.Arrange(new Rect(finalSize.Width * 0.5 - child.DesiredSize.Width * 0.5, finalSize.Height * 0.5 - child.DesiredSize.Height * 0.5, child.DesiredSize.Width, child.DesiredSize.Height));
            
        }

        

        /// <summary>
        /// Apply the layout algorithm to the specified item
        /// </summary>
        /// <param name="normalizedIndex">index to apply</param>
        /// <param name="element">item to layout</param>
        /// <param name="consideredItemsCount">considered items count to pass to the algorithm</param>
        protected abstract void ApplyLayout(double normalizedIndex, UIElement element, int consideredItemsCount);

        /// <summary>
        /// Calculate the normalized index of the specified itemIndex
        /// </summary>
        /// <param name="itemIndex"></param>
        /// <returns></returns>
        double CalculateRelativeIndex(int itemIndex)
        {
            double relativeIndex = itemIndex - _layoutOffset;
            if (relativeIndex < 0)
                relativeIndex += _itemCount;
            if (Math.Abs(relativeIndex - _itemCount) < Math.Abs(relativeIndex))
                relativeIndex = relativeIndex - ItemCount;
            return relativeIndex * 2.0 / _consideredItems;
        }

        

        private int _usedRanges;
        int[] _visibleRanges=new int[6];
        /// <summary>
        /// Specify if the specified container index in the visible range
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        private bool IsInVisibleRange(int index)
        {
            for(int i=0;i<_usedRanges;i++)
            {
                if (index >= _visibleRanges[i * 2] && index <= _visibleRanges[i * 2 + 1])
                    return true;
            }
            return false;
        }

        /// <summary>
        /// Compute the visible ranges
        /// </summary>
        /// <returns></returns>
        private void UpdateVisibleRanges()
        {
            _usedRanges = 0;
            if (_firstVisibleIndex < 0)
            {
                ++_usedRanges;
                _visibleRanges[0] = ItemCount + _firstVisibleIndex;
                _visibleRanges[1] = ItemCount - 1;
            }
            _visibleRanges[2 * _usedRanges] = _firstVisibleIndex > 0 ? _firstVisibleIndex : 0;
            _visibleRanges[2 * _usedRanges + 1] = _lastVisibleIndex >= ItemCount ? ItemCount - 1 : _lastVisibleIndex;
            ++_usedRanges;
            if (_lastVisibleIndex >= ItemCount)
            {
                _visibleRanges[2*_usedRanges] = 0;
                _visibleRanges[2*_usedRanges + 1] = _lastVisibleIndex - ItemCount;
                _usedRanges++;
            }
        }

       

        
        /// <summary>
        /// Get the maximum item count displayed at the same time by the layout algorithm
        /// </summary>
        /// <returns></returns>
        protected abstract int GetAlgorithmMaxDisplayedItems();

        /// <summary>
        /// Calculate the first and last visible indices
        /// </summary>
        private void CalculateIndices()
        {
            int layoutMax = GetAlgorithmMaxDisplayedItems();
            if (layoutMax == -1)
                return;

            var totalItems = Math.Min(layoutMax, ItemCount);
            _consideredItems = totalItems;
            double halfTotal = 0.5 * totalItems;
            _firstVisibleIndex = (int)Math.Ceiling(_layoutOffset - halfTotal);
            _lastVisibleIndex = (int)Math.Floor(_layoutOffset + halfTotal);

            if (CyclicMode)
            {
                if (_lastVisibleIndex - _firstVisibleIndex < _consideredItems)
                    _lastVisibleIndex++;
            }
            if (_lastVisibleIndex - _firstVisibleIndex > _consideredItems)
                _lastVisibleIndex--;
            _consideredItems = _lastVisibleIndex - _firstVisibleIndex;
            while(_firstVisibleIndex<= -ItemCount)
            {
                _firstVisibleIndex += ItemCount;
                _lastVisibleIndex += ItemCount;
            }
            while(_firstVisibleIndex>=ItemCount)
            {
                _firstVisibleIndex -= ItemCount;
                _lastVisibleIndex -= ItemCount;
            }
            if (!_cyclicMode)
            {

                if (_lastVisibleIndex >= ItemCount - 1)
                    _lastVisibleIndex = ItemCount - 1;
                if (_firstVisibleIndex < 0)
                    _firstVisibleIndex = 0;
            }
        }

    }

    public abstract class NotVirtualizedFlowPanelBase : Panel
    {
        private double _layoutOffset;
        private int _itemCount;
        private int _firstVisibleIndex, _lastVisibleIndex;

        /// <summary>
        /// Get or set the Layout offset
        /// </summary>
        public double LayoutOffset
        {
            get { return _layoutOffset; }
            set
            {
                _layoutOffset = value;
                InvalidateMeasure();
            }
        }

        private bool _cyclicMode;

        /// <summary>
        /// Get or set the Cyclic mode
        /// </summary>
        public bool CyclicMode
        {
            get { return _cyclicMode; }
            set
            {
                _cyclicMode = value;
                InvalidateMeasure();
            }
        }


        /// <summary>
        /// Get or set the total item count
        /// </summary>
        public int ItemCount
        {
            get { return _itemCount; }
            set
            {
                _itemCount = value;
                InvalidateMeasure();
            }
        }

        private double _consideredItems;

        /// <summary>
        /// Get or set the number of items to consider in a layout pass
        /// </summary>
        public double ConsideredItems
        {
            get { return _consideredItems; }
            set { _consideredItems = value; }
        }

        /// <summary>
        /// Send the size of the viewport to the underlaying layout algorithm
        /// </summary>
        /// <param name="availableSize"></param>
        protected abstract void SendAvailableSizeToAlgorithm(Size availableSize);


        /// <summary>
        /// Provides the behavior for the Measure pass of Silverlight layout. Classes can override this method to define their own Measure pass behavior.
        /// </summary>
        /// <returns>
        /// The size that this object determines it needs during layout, based on its calculations of child object allotted sizes, or possibly on other considerations such as fixed container size.
        /// </returns>
        /// <param name="availableSize">The available size that this object can give to child objects. Infinity (<see cref="F:System.Double.PositiveInfinity"/>) can be specified as a value to indicate that the object will size to whatever content is available.</param>
        protected override Size MeasureOverride(Size availableSize)
        {
            // layout only work with a constrained size
            if (double.IsPositiveInfinity(availableSize.Width) || double.IsPositiveInfinity(availableSize.Height) || ItemCount==0)
            {
                return new Size(0, 0);
            }

            SendAvailableSizeToAlgorithm(availableSize);

            CalculateIndices();

            UpdateVisibleRanges();


            // get the visible ranges and apply layout on them

            for (int i = 0; i < Children.Count; i++)
            {
                if (IsInVisibleRange(i))
                {
                    var child = Children[i];
                    child.Measure(availableSize);

                    double relativeItemIndex = CalculateRelativeIndex(i);

                    ApplyLayout(relativeItemIndex, child, (int)_consideredItems);
                    child.IsHitTestVisible = true;
                }
                else
                {
                    var child = Children[i];
                    child.Opacity = 0;
                    child.IsHitTestVisible = false;
                }
            }




            ApplyZOrder(VisibleChildren);


            return availableSize;
        }


        IEnumerable<UIElement> VisibleChildren
        {
            get
            {
                for (int i = 0; i < _usedRanges; i++)
                {
                    for (int index = _visibleRanges[i * 2]; index <= _visibleRanges[i * 2 + 1]; index++)
                    {
                        if(index>=0 && index<Children.Count)
                            yield return Children[index];
                    }
                }
            }
        }

        /// <summary>
        /// Apply the Z order to the flow items
        /// </summary>
        /// <param name="children"></param>
        protected abstract void ApplyZOrder(IEnumerable<UIElement> children);




        /// <summary>
        /// Provides the behavior for the Arrange pass of Silverlight layout. Classes can override this method to define their own Arrange pass behavior.
        /// </summary>
        /// <returns>
        /// The actual size used once the element is arranged in layout.
        /// </returns>
        /// <param name="finalSize">The final area within the parent that this object should use to arrange itself and its children.</param>
        protected override Size ArrangeOverride(Size finalSize)
        {
            foreach (var child in VisibleChildren)
            {
                ArrangeChild(finalSize, child);
            }
            return finalSize;
        }


        /// <summary>
        /// Arrange a child item
        /// </summary>
        /// <param name="finalSize">size of the viewport</param>
        /// <param name="child">item to draw</param>
        protected virtual void ArrangeChild(Size finalSize, UIElement child)
        {
            if (child.DesiredSize.Height == 0.0 || child.DesiredSize.Width == 0.0)
                return;
            child.Arrange(new Rect(finalSize.Width * 0.5 - child.DesiredSize.Width * 0.5, finalSize.Height * 0.5 - child.DesiredSize.Height * 0.5, child.DesiredSize.Width, child.DesiredSize.Height));

        }



        /// <summary>
        /// Apply the layout algorithm to the specified item
        /// </summary>
        /// <param name="normalizedIndex">index to apply</param>
        /// <param name="element">item to layout</param>
        /// <param name="consideredItemsCount">considered items count to pass to the algorithm</param>
        protected abstract void ApplyLayout(double normalizedIndex, UIElement element, int consideredItemsCount);

        /// <summary>
        /// Calculate the normalized index of the specified itemIndex
        /// </summary>
        /// <param name="itemIndex"></param>
        /// <returns></returns>
        double CalculateRelativeIndex(int itemIndex)
        {
            double relativeIndex = itemIndex - _layoutOffset;
            if (relativeIndex < 0)
                relativeIndex += _itemCount;
            if (Math.Abs(relativeIndex - _itemCount) < Math.Abs(relativeIndex))
                relativeIndex = relativeIndex - ItemCount;
            return relativeIndex * 2.0 / _consideredItems;
        }



        /// <summary>
        /// Get the item index corresponding to the specified container
        /// </summary>
        /// <param name="itemsControl"></param>
        /// <param name="realizedChild"></param>
        /// <returns></returns>
        protected virtual int GetItemIndexOfContainer(ItemsControl itemsControl, UIElement realizedChild)
        {
            return itemsControl.Items.IndexOf(((ContentControl)realizedChild).Content);
        }



        private int _usedRanges;
        int[] _visibleRanges = new int[6];
        /// <summary>
        /// Specify if the specified container index in the visible range
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        private bool IsInVisibleRange(int index)
        {
            for (int i = 0; i < _usedRanges; i++)
            {
                if (index >= _visibleRanges[i * 2] && index <= _visibleRanges[i * 2 + 1])
                    return true;
            }
            return false;
        }

        /// <summary>
        /// Compute the visible ranges
        /// </summary>
        /// <returns></returns>
        private void UpdateVisibleRanges()
        {
            _usedRanges = 0;
            if (_firstVisibleIndex < 0)
            {
                ++_usedRanges;
                _visibleRanges[0] = ItemCount + _firstVisibleIndex;
                _visibleRanges[1] = ItemCount - 1;
            }
            _visibleRanges[2 * _usedRanges] = _firstVisibleIndex > 0 ? _firstVisibleIndex : 0;
            _visibleRanges[2 * _usedRanges + 1] = _lastVisibleIndex >= ItemCount ? ItemCount - 1 : _lastVisibleIndex;
            ++_usedRanges;
            if (_lastVisibleIndex >= ItemCount)
            {
                _visibleRanges[2 * _usedRanges] = 0;
                _visibleRanges[2 * _usedRanges + 1] = _lastVisibleIndex - ItemCount;
                _usedRanges++;
            }
        }



        /// <summary>
        /// Get the maximum item count displayed at the same time by the layout algorithm
        /// </summary>
        /// <returns></returns>
        protected abstract int GetAlgorithmMaxDisplayedItems();

        /// <summary>
        /// Calculate the first and last visible indices
        /// </summary>
        private void CalculateIndices()
        {
            int layoutMax = GetAlgorithmMaxDisplayedItems();
            if (layoutMax == -1)
                return;

            var totalItems = Math.Min(layoutMax, ItemCount);
            _consideredItems = totalItems;
            double halfTotal = 0.5 * totalItems;
            _firstVisibleIndex = (int)Math.Ceiling(_layoutOffset - halfTotal);
            _lastVisibleIndex = (int)Math.Floor(_layoutOffset + halfTotal);
            if (_lastVisibleIndex - _firstVisibleIndex < _consideredItems)
                _lastVisibleIndex++;
            else if (_lastVisibleIndex - _firstVisibleIndex > _consideredItems)
                _lastVisibleIndex--;
            _consideredItems = _lastVisibleIndex - _firstVisibleIndex;
            if (!_cyclicMode)
            {
                if (_firstVisibleIndex < 0)
                    _firstVisibleIndex = 0;
                if (_lastVisibleIndex >= ItemCount)
                    _lastVisibleIndex = ItemCount - 1;
            }
        }

    }
}
